// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/views/bubble/tray_bubble_view.h"

#include <algorithm>

#include "base/macros.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/effects/SkBlurImageFilter.h"
#include "ui/accessibility/ax_view_state.h"
#include "ui/aura/window.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_delegate.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/events/event.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/path.h"
#include "ui/gfx/skia_util.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/bubble/bubble_window_targeter.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/widget/widget.h"

namespace {

// Inset the arrow a bit from the edge.
const int kArrowMinOffset = 20;
const int kBubbleSpacing = 20;

// The new theme adjusts the menus / bubbles to be flush with the shelf when
// there is no bubble. These are the offsets which need to be applied.
const int kArrowOffsetTopBottom = 4;
const int kArrowOffsetLeft = 9;
const int kArrowOffsetRight = -5;
const int kOffsetLeftRightForTopBottomOrientation = 5;

// The sampling time for mouse position changes in ms - which is roughly a frame
// time.
const int kFrameTimeInMS = 30;
} // namespace

namespace views {

namespace internal {

    // Detects any mouse movement. This is needed to detect mouse movements by the
    // user over the bubble if the bubble got created underneath the cursor.
    class MouseMoveDetectorHost : public MouseWatcherHost {
    public:
        MouseMoveDetectorHost();
        ~MouseMoveDetectorHost() override;

        bool Contains(const gfx::Point& screen_point, MouseEventType type) override;

    private:
        DISALLOW_COPY_AND_ASSIGN(MouseMoveDetectorHost);
    };

    MouseMoveDetectorHost::MouseMoveDetectorHost()
    {
    }

    MouseMoveDetectorHost::~MouseMoveDetectorHost()
    {
    }

    bool MouseMoveDetectorHost::Contains(const gfx::Point& screen_point,
        MouseEventType type)
    {
        return false;
    }

    // Custom border for TrayBubbleView. Contains special logic for GetBounds()
    // to stack bubbles with no arrows correctly. Also calculates the arrow offset.
    class TrayBubbleBorder : public BubbleBorder {
    public:
        TrayBubbleBorder(View* owner,
            View* anchor,
            TrayBubbleView::InitParams params)
            : BubbleBorder(params.arrow, params.shadow, params.arrow_color)
            , owner_(owner)
            , anchor_(anchor)
            , tray_arrow_offset_(params.arrow_offset)
            , first_item_has_no_margin_(params.first_item_has_no_margin)
        {
            set_alignment(params.arrow_alignment);
            set_background_color(params.arrow_color);
            set_paint_arrow(params.arrow_paint_type);
        }

        ~TrayBubbleBorder() override { }

        // Overridden from BubbleBorder.
        // Sets the bubble on top of the anchor when it has no arrow.
        gfx::Rect GetBounds(const gfx::Rect& position_relative_to,
            const gfx::Size& contents_size) const override
        {
            if (has_arrow(arrow())) {
                gfx::Rect rect = BubbleBorder::GetBounds(position_relative_to, contents_size);
                if (first_item_has_no_margin_) {
                    if (arrow() == BubbleBorder::BOTTOM_RIGHT || arrow() == BubbleBorder::BOTTOM_LEFT) {
                        rect.set_y(rect.y() + kArrowOffsetTopBottom);
                        int rtl_factor = base::i18n::IsRTL() ? -1 : 1;
                        rect.set_x(rect.x() + rtl_factor * kOffsetLeftRightForTopBottomOrientation);
                    } else if (arrow() == BubbleBorder::LEFT_BOTTOM) {
                        rect.set_x(rect.x() + kArrowOffsetLeft);
                    } else if (arrow() == BubbleBorder::RIGHT_BOTTOM) {
                        rect.set_x(rect.x() + kArrowOffsetRight);
                    }
                }
                return rect;
            }

            gfx::Size border_size(contents_size);
            gfx::Insets insets = GetInsets();
            border_size.Enlarge(insets.width(), insets.height());
            const int x = position_relative_to.x() + position_relative_to.width() / 2 - border_size.width() / 2;
            // Position the bubble on top of the anchor.
            const int y = position_relative_to.y() - border_size.height() + insets.height() - kBubbleSpacing;
            return gfx::Rect(x, y, border_size.width(), border_size.height());
        }

        void UpdateArrowOffset()
        {
            int arrow_offset = 0;
            if (arrow() == BubbleBorder::BOTTOM_RIGHT || arrow() == BubbleBorder::BOTTOM_LEFT) {
                // Note: tray_arrow_offset_ is relative to the anchor widget.
                if (tray_arrow_offset_ == TrayBubbleView::InitParams::kArrowDefaultOffset) {
                    arrow_offset = kArrowMinOffset;
                } else {
                    const int width = owner_->GetWidget()->GetContentsView()->width();
                    gfx::Point pt(tray_arrow_offset_, 0);
                    View::ConvertPointToScreen(anchor_->GetWidget()->GetRootView(), &pt);
                    View::ConvertPointFromScreen(owner_->GetWidget()->GetRootView(), &pt);
                    arrow_offset = pt.x();
                    if (arrow() == BubbleBorder::BOTTOM_RIGHT)
                        arrow_offset = width - arrow_offset;
                    arrow_offset = std::max(arrow_offset, kArrowMinOffset);
                }
            } else {
                if (tray_arrow_offset_ == TrayBubbleView::InitParams::kArrowDefaultOffset) {
                    arrow_offset = kArrowMinOffset;
                } else {
                    gfx::Point pt(0, tray_arrow_offset_);
                    View::ConvertPointToScreen(anchor_->GetWidget()->GetRootView(), &pt);
                    View::ConvertPointFromScreen(owner_->GetWidget()->GetRootView(), &pt);
                    arrow_offset = pt.y();
                    arrow_offset = std::max(arrow_offset, kArrowMinOffset);
                }
            }
            set_arrow_offset(arrow_offset);
        }

    private:
        View* owner_;
        View* anchor_;
        const int tray_arrow_offset_;

        // If true the first item should not get any additional spacing against the
        // anchor (without the bubble tip the bubble should be flush to the shelf).
        const bool first_item_has_no_margin_;

        DISALLOW_COPY_AND_ASSIGN(TrayBubbleBorder);
    };

    // This mask layer clips the bubble's content so that it does not overwrite the
    // rounded bubble corners.
    // TODO(miket): This does not work on Windows. Implement layer masking or
    // alternate solutions if the TrayBubbleView is needed there in the future.
    class TrayBubbleContentMask : public ui::LayerDelegate {
    public:
        explicit TrayBubbleContentMask(int corner_radius);
        ~TrayBubbleContentMask() override;

        ui::Layer* layer() { return &layer_; }

        // Overridden from LayerDelegate.
        void OnPaintLayer(const ui::PaintContext& context) override;
        void OnDelegatedFrameDamage(const gfx::Rect& damage_rect_in_dip) override { }
        void OnDeviceScaleFactorChanged(float device_scale_factor) override;
        base::Closure PrepareForLayerBoundsChange() override;

    private:
        ui::Layer layer_;
        int corner_radius_;

        DISALLOW_COPY_AND_ASSIGN(TrayBubbleContentMask);
    };

    TrayBubbleContentMask::TrayBubbleContentMask(int corner_radius)
        : layer_(ui::LAYER_TEXTURED)
        , corner_radius_(corner_radius)
    {
        layer_.set_delegate(this);
    }

    TrayBubbleContentMask::~TrayBubbleContentMask()
    {
        layer_.set_delegate(NULL);
    }

    void TrayBubbleContentMask::OnPaintLayer(const ui::PaintContext& context)
    {
        ui::PaintRecorder recorder(context, layer()->size());
        SkPaint paint;
        paint.setAlpha(255);
        paint.setStyle(SkPaint::kFill_Style);
        gfx::Rect rect(layer()->bounds().size());
        recorder.canvas()->DrawRoundRect(rect, corner_radius_, paint);
    }

    void TrayBubbleContentMask::OnDeviceScaleFactorChanged(
        float device_scale_factor)
    {
        // Redrawing will take care of scale factor change.
    }

    base::Closure TrayBubbleContentMask::PrepareForLayerBoundsChange()
    {
        return base::Closure();
    }

    // Custom layout for the bubble-view. Does the default box-layout if there is
    // enough height. Otherwise, makes sure the bottom rows are visible.
    class BottomAlignedBoxLayout : public BoxLayout {
    public:
        explicit BottomAlignedBoxLayout(TrayBubbleView* bubble_view)
            : BoxLayout(BoxLayout::kVertical, 0, 0, 0)
            , bubble_view_(bubble_view)
        {
        }

        ~BottomAlignedBoxLayout() override { }

    private:
        void Layout(View* host) override
        {
            if (host->height() >= host->GetPreferredSize().height() || !bubble_view_->is_gesture_dragging()) {
                BoxLayout::Layout(host);
                return;
            }

            int consumed_height = 0;
            for (int i = host->child_count() - 1;
                 i >= 0 && consumed_height < host->height(); --i) {
                View* child = host->child_at(i);
                if (!child->visible())
                    continue;
                gfx::Size size = child->GetPreferredSize();
                child->SetBounds(0, host->height() - consumed_height - size.height(),
                    host->width(), size.height());
                consumed_height += size.height();
            }
        }

        TrayBubbleView* bubble_view_;

        DISALLOW_COPY_AND_ASSIGN(BottomAlignedBoxLayout);
    };

} // namespace internal

using internal::BottomAlignedBoxLayout;
using internal::TrayBubbleBorder;
using internal::TrayBubbleContentMask;

// static
const int TrayBubbleView::InitParams::kArrowDefaultOffset = -1;

TrayBubbleView::InitParams::InitParams(AnchorType anchor_type,
    AnchorAlignment anchor_alignment,
    int min_width,
    int max_width)
    : anchor_type(anchor_type)
    , anchor_alignment(anchor_alignment)
    , min_width(min_width)
    , max_width(max_width)
    , max_height(0)
    , can_activate(false)
    , close_on_deactivate(true)
    , arrow_color(SK_ColorBLACK)
    , first_item_has_no_margin(false)
    , arrow(BubbleBorder::NONE)
    , arrow_offset(kArrowDefaultOffset)
    , arrow_paint_type(BubbleBorder::PAINT_NORMAL)
    , shadow(BubbleBorder::BIG_SHADOW)
    , arrow_alignment(BubbleBorder::ALIGN_EDGE_TO_ANCHOR_EDGE)
{
}

// static
TrayBubbleView* TrayBubbleView::Create(gfx::NativeView parent_window,
    View* anchor,
    Delegate* delegate,
    InitParams* init_params)
{
    // Set arrow here so that it can be passed to the BubbleView constructor.
    if (init_params->anchor_type == ANCHOR_TYPE_TRAY) {
        if (init_params->anchor_alignment == ANCHOR_ALIGNMENT_BOTTOM) {
            init_params->arrow = base::i18n::IsRTL() ? BubbleBorder::BOTTOM_LEFT : BubbleBorder::BOTTOM_RIGHT;
        } else if (init_params->anchor_alignment == ANCHOR_ALIGNMENT_TOP) {
            init_params->arrow = BubbleBorder::TOP_LEFT;
        } else if (init_params->anchor_alignment == ANCHOR_ALIGNMENT_LEFT) {
            init_params->arrow = BubbleBorder::LEFT_BOTTOM;
        } else {
            init_params->arrow = BubbleBorder::RIGHT_BOTTOM;
        }
    } else {
        init_params->arrow = BubbleBorder::NONE;
    }

    return new TrayBubbleView(parent_window, anchor, delegate, *init_params);
}

TrayBubbleView::TrayBubbleView(gfx::NativeView parent_window,
    View* anchor,
    Delegate* delegate,
    const InitParams& init_params)
    : BubbleDelegateView(anchor, init_params.arrow)
    , params_(init_params)
    , delegate_(delegate)
    , preferred_width_(init_params.min_width)
    , bubble_border_(new TrayBubbleBorder(this, GetAnchorView(), init_params))
    , owned_bubble_border_(bubble_border_)
    , is_gesture_dragging_(false)
    , mouse_actively_entered_(false)
{
    set_parent_window(parent_window);
    set_notify_enter_exit_on_child(true);
    set_close_on_deactivate(init_params.close_on_deactivate);
    set_margins(gfx::Insets());
    SetPaintToLayer(true);
    SetFillsBoundsOpaquely(true);

    bubble_content_mask_.reset(
        new TrayBubbleContentMask(bubble_border_->GetBorderCornerRadius()));
}

TrayBubbleView::~TrayBubbleView()
{
    mouse_watcher_.reset();
    // Inform host items (models) that their views are being destroyed.
    if (delegate_)
        delegate_->BubbleViewDestroyed();
}

void TrayBubbleView::InitializeAndShowBubble()
{
    // Must occur after call to BubbleDelegateView::CreateBubble().
    SetAlignment(params_.arrow_alignment);
    bubble_border_->UpdateArrowOffset();

    layer()->parent()->SetMaskLayer(bubble_content_mask_->layer());

    GetWidget()->Show();
    GetWidget()->GetNativeWindow()->SetEventTargeter(
        scoped_ptr<ui::EventTargeter>(new BubbleWindowTargeter(this)));
    UpdateBubble();
}

void TrayBubbleView::UpdateBubble()
{
    if (GetWidget()) {
        SizeToContents();
        bubble_content_mask_->layer()->SetBounds(layer()->bounds());
        GetWidget()->GetRootView()->SchedulePaint();
    }
}

void TrayBubbleView::SetMaxHeight(int height)
{
    params_.max_height = height;
    if (GetWidget())
        SizeToContents();
}

void TrayBubbleView::SetWidth(int width)
{
    width = std::max(std::min(width, params_.max_width), params_.min_width);
    if (preferred_width_ == width)
        return;
    preferred_width_ = width;
    if (GetWidget())
        SizeToContents();
}

void TrayBubbleView::SetArrowPaintType(
    views::BubbleBorder::ArrowPaintType paint_type)
{
    bubble_border_->set_paint_arrow(paint_type);
    UpdateBubble();
}

gfx::Insets TrayBubbleView::GetBorderInsets() const
{
    return bubble_border_->GetInsets();
}

void TrayBubbleView::Init()
{
    BoxLayout* layout = new BottomAlignedBoxLayout(this);
    layout->SetDefaultFlex(1);
    SetLayoutManager(layout);
}

gfx::Rect TrayBubbleView::GetAnchorRect() const
{
    if (!delegate_)
        return gfx::Rect();
    return delegate_->GetAnchorRect(anchor_widget(),
        params_.anchor_type,
        params_.anchor_alignment);
}

bool TrayBubbleView::CanActivate() const
{
    return params_.can_activate;
}

NonClientFrameView* TrayBubbleView::CreateNonClientFrameView(Widget* widget)
{
    BubbleFrameView* frame = static_cast<BubbleFrameView*>(
        BubbleDelegateView::CreateNonClientFrameView(widget));
    frame->SetBubbleBorder(std::move(owned_bubble_border_));
    return frame;
}

bool TrayBubbleView::WidgetHasHitTestMask() const
{
    return true;
}

void TrayBubbleView::GetWidgetHitTestMask(gfx::Path* mask) const
{
    DCHECK(mask);
    mask->addRect(gfx::RectToSkRect(GetBubbleFrameView()->GetContentsBounds()));
}

gfx::Size TrayBubbleView::GetPreferredSize() const
{
    return gfx::Size(preferred_width_, GetHeightForWidth(preferred_width_));
}

gfx::Size TrayBubbleView::GetMaximumSize() const
{
    gfx::Size size = GetPreferredSize();
    size.set_width(params_.max_width);
    return size;
}

int TrayBubbleView::GetHeightForWidth(int width) const
{
    int height = GetInsets().height();
    width = std::max(width - GetInsets().width(), 0);
    for (int i = 0; i < child_count(); ++i) {
        const View* child = child_at(i);
        if (child->visible())
            height += child->GetHeightForWidth(width);
    }

    return (params_.max_height != 0) ? std::min(height, params_.max_height) : height;
}

void TrayBubbleView::OnMouseEntered(const ui::MouseEvent& event)
{
    mouse_watcher_.reset();
    if (delegate_ && !(event.flags() & ui::EF_IS_SYNTHESIZED)) {
        // Coming here the user was actively moving the mouse over the bubble and
        // we inform the delegate that we entered. This will prevent the bubble
        // to auto close.
        delegate_->OnMouseEnteredView();
        mouse_actively_entered_ = true;
    } else {
        // Coming here the bubble got shown and the mouse was 'accidentally' over it
        // which is not a reason to prevent the bubble to auto close. As such we
        // do not call the delegate, but wait for the first mouse move within the
        // bubble. The used MouseWatcher will notify use of a movement and call
        // |MouseMovedOutOfHost|.
        mouse_watcher_.reset(new MouseWatcher(
            new views::internal::MouseMoveDetectorHost(),
            this));
        // Set the mouse sampling frequency to roughly a frame time so that the user
        // cannot see a lag.
        mouse_watcher_->set_notify_on_exit_time(
            base::TimeDelta::FromMilliseconds(kFrameTimeInMS));
        mouse_watcher_->Start();
    }
}

void TrayBubbleView::OnMouseExited(const ui::MouseEvent& event)
{
    // If there was a mouse watcher waiting for mouse movements we disable it
    // immediately since we now leave the bubble.
    mouse_watcher_.reset();
    // Do not notify the delegate of an exit if we never told it that we entered.
    if (delegate_ && mouse_actively_entered_)
        delegate_->OnMouseExitedView();
}

void TrayBubbleView::GetAccessibleState(ui::AXViewState* state)
{
    if (delegate_ && params_.can_activate) {
        state->role = ui::AX_ROLE_WINDOW;
        state->name = delegate_->GetAccessibleNameForBubble();
    }
}

void TrayBubbleView::MouseMovedOutOfHost()
{
    // The mouse was accidentally over the bubble when it opened and the AutoClose
    // logic was not activated. Now that the user did move the mouse we tell the
    // delegate to disable AutoClose.
    delegate_->OnMouseEnteredView();
    mouse_actively_entered_ = true;
    mouse_watcher_->Stop();
}

void TrayBubbleView::ChildPreferredSizeChanged(View* child)
{
    SizeToContents();
}

void TrayBubbleView::ViewHierarchyChanged(
    const ViewHierarchyChangedDetails& details)
{
    if (details.is_add && details.child == this) {
        details.parent->SetPaintToLayer(true);
        details.parent->SetFillsBoundsOpaquely(true);
        details.parent->layer()->SetMasksToBounds(true);
    }
}

} // namespace views
